﻿using System;
using System.Linq.Expressions;
using System.Reflection;

namespace Nito.AsyncEx.Internal.PlatformEnlightenment
{
    partial class DefaultEnlightenmentProvider
    {
        /// <summary>
        /// The default exception enlightenment, which will use <c>ExceptionDispatchInfo</c> if possible, falling back on <c>Exception.PrepForRemoting</c>, with a final fallback on <see cref="Exception.Data"/>.
        /// </summary>
        public sealed class ExceptionEnlightenment : IExceptionEnlightenment
        {
            /// <summary>
            /// A delegate that will call <c>ExceptionDispatchInfo.Capture</c> followed by <c>ExceptionDispatchInfo.Throw</c>, or <c>null</c> if the <c>ExceptionDispatchInfo</c> type does not exist.
            /// </summary>
            private readonly Action<Exception> _captureAndThrow;

            /// <summary>
            /// A delegate that will call <c>Exception.PrepForRemoting</c>, or <c>null</c> if the method does not exist. This member is always <c>null</c> if <see cref="_captureAndThrow"/> is non-<c>null</c>.
            /// </summary>
            private readonly Action<Exception> _prepForRemoting;

            /// <summary>
            /// Examines the current runtime and initializes the delegates appropriately.
            /// </summary>
            public ExceptionEnlightenment(IReflectionExpressionProvider r)
            {
                var exception = Expression.Parameter(typeof(Exception), "exception");
                var capture = r.Call(r.Type("System.Runtime.ExceptionServices.ExceptionDispatchInfo"), "Capture", exception);
                var @throw = r.Call(capture, "Throw");
                _captureAndThrow = r.Compile<Action<Exception>>(@throw, exception);

                var prepForRemoting = r.Call(exception, "PrepForRemoting", BindingFlags.Instance | BindingFlags.NonPublic);
                _prepForRemoting = r.Compile<Action<Exception>>(prepForRemoting, exception);
            }

            /// <summary>
            /// Attempts to add the original stack trace to the <see cref="Exception.Data"/> collection.
            /// </summary>
            /// <param name="exception">The exception. May not be <c>null</c>.</param>
            /// <returns><c>true</c> if the stack trace was successfully saved; <c>false</c> otherwise.</returns>
            private static void TryAddStackTrace(Exception exception)
            {
                try
                {
                    exception.Data.Add("Original stack trace", exception.StackTrace);
                }
                catch (ArgumentException)
                {
                    // Vexing exception
                }
                catch (NotSupportedException)
                {
                    // Vexing exception
                }
            }

            Exception IExceptionEnlightenment.PrepareForRethrow(Exception exception)
            {
                if (_captureAndThrow != null)
                {
                    _captureAndThrow(exception);
                }
                else if (_prepForRemoting != null)
                {
                    _prepForRemoting(exception);
                }
                else
                {
                    TryAddStackTrace(exception);
                }

                return exception;
            }
        }
    }
}
